using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Audio;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.GamerServices;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Input;
using Microsoft.Xna.Framework.Media;
using Microsoft.Xna.Framework.Net;
using Microsoft.Xna.Framework.Storage;

namespace Dungeon_Dive
{    

    /// <summary>
    /// This game component is responsible for managing game objects
    /// and most of the game logic specific to Dungeon Dive.
    /// </summary>
    public class LogicEngine : DrawableGameComponent
    {
        Game1 parentGame;

        public Camera Camera { get; set; }

        public TriDTile[,] Tiles { get; set; }        

        public PlayerCharacter TPC { get; set; }        
        public List<GameEntity> Entities { get; set; }
        List<GameEntity> dead;
        public List<GameEntity> Born { get; set; }
        Dictionary<String, SoundEffect> soundEffects;

        MouseState oldMouseState;
        KeyboardState oldKeyboardState;
        Random random;
        PythonEngine pyEngine;
        XMLEngine xmlEngine;
        HealthBar healthbar;
        Log log;
        Console console;

        SpriteBatch sb;
        SpriteFont font;

        bool stepSoundPlayed;


        // variables for game events

        bool playerAttack;

        bool activatedPortal;
        bool activatedElementalPortal;

        ElementToken tokenPickUp;
        TileTypes activePortal;



        int levelNumber;
        public int LevelNumber
        { 
            get { return levelNumber; }
            set
            {
                if (value >= 0)
                    levelNumber = value;
                else
                    value = 0;
            }
        }
        
        Vector3 target;
        public Vector3 PortalPos { get; set; }

        //In Level for now, should really go to PlayerCharacter or GameEntity
        TimeSpan attackDelay;
        TimeSpan elapsedTime;
        
        bool changeLevel;

        public bool RenderBoundingSpheres { get; set; }
        public bool Deathless { get; set; }

        public LogicEngine(Game1 game)
            : base(game)
        {            
            parentGame = game;

            pyEngine = (PythonEngine)parentGame.Services.GetService(typeof(PythonEngine));
            pyEngine.SetVariable("level", this);
            pyEngine.AddScriptFromFile("Level generator", ".\\scripts\\levelgen.py");
            pyEngine.AddScriptFromFile("Starting level generator", ".\\scripts\\startlevelgen.py");

            xmlEngine = (XMLEngine)parentGame.Services.GetService(typeof(XMLEngine));

            TPC = new PlayerCharacter(parentGame, "hero");                       
            TPC.Play();

            font = parentGame.Content.Load<SpriteFont>("Fonts\\Debugfont");

            log = (Log)game.Services.GetService(typeof(Log));

            healthbar = new HealthBar(parentGame);

            Entities = new List<GameEntity>();
            dead = new List<GameEntity>();
            Born = new List<GameEntity>();            

            attackDelay = TimeSpan.FromMilliseconds(500);   // what's in a magic number?
            elapsedTime = TimeSpan.Zero;

            console = (Console)game.Services.GetService(typeof(Console));
            console.Level = this;
            console.Visible = false;
            console.Enabled = false;

            target = TPC.Position;
            RenderBoundingSpheres = false;

            Deathless = false;  // for cheating purposes

            GenerateLevel();
        }

        /// <summary>
        /// Initialize level
        /// </summary>
        public override void Initialize()
        {
            log.Write("Level.Init()");
            Camera = new Camera(parentGame, new Vector3(2000, 2000, 2000),
                                Vector3.Zero, Vector3.Up);
            
            random = (Random)parentGame.Services.GetService(typeof(Random));
            healthbar.Initialize();
            base.Initialize();
        }

        /// <summary>
        /// Most of the game action is managed here
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Update(GameTime gameTime)
        {
            //Here we position the camera behind and above the player
            target = TPC.Position;
            if (!Camera.Free)
            {
                Vector3 cameraPosition = Camera.Position;
                cameraPosition.Y = 800;
                cameraPosition.X = TPC.Position.X - (float)(1200 * Math.Sin((double)TPC.Rotation));
                cameraPosition.Z = TPC.Position.Z - (float)(1200 * Math.Cos((double)TPC.Rotation));
                Camera.Position = cameraPosition;
                Camera.Target = target;
            }
            Camera.Update(gameTime);

            elapsedTime += gameTime.ElapsedGameTime;      // for attack delay      
            
            HandleInput();

            if (playerAttack)
            {
                if (elapsedTime > attackDelay)
                {
                    if (TPC.CurrentAnim != "attack")
                    {
                        TPC.PlayOnce("attack");
                    }
                    foreach (GameEntity NPC in Entities)
                    {
                        if (TPC.GetAttackVolume().Intersects(NPC.BoundingSphere))
                        {
                            soundEffects["swordhit"].Play();
                            if (NPC.HandleHit(TPC.AttackType))  // if monster dead
                            {
                                dead.Add(NPC);
                                if (NPC.GetType() == typeof(Monster))
                                {
                                    Monster m = (Monster)NPC;
                                    ElementToken nt = new ElementToken(parentGame, m.GetElement());
                                    nt.Position = new Vector3(m.Position.X, 100, m.Position.Z);
                                    Born.Add(nt);   // monsters leave element tokens
                                }
                            }
                        }
                    }
                    elapsedTime = TimeSpan.Zero;   // reset attack delay
                }
                playerAttack = false;
            }

            if (tokenPickUp != null)
            {                                
                TPC.AddToken(tokenPickUp.GetElement());
                dead.Add(tokenPickUp);
                soundEffects["powerup"].Play();
                tokenPickUp = null;
            }

            if (activatedPortal)
            {
                changeLevel = true;                

                if (activatedElementalPortal)
                {
                    if (levelNumber > 0)
                    {
                        ++levelNumber;
                        if (levelNumber > TPC.Progress) //We've reached a new checkpoint
                        {
                            xmlEngine.CreateFile(".\\XML\\progress.xml");
                            xmlEngine.StartElement("progress");
                            xmlEngine.StartElement("level");
                            xmlEngine.AddAttribute("value", levelNumber.ToString());
                            xmlEngine.EndDocument();
                        }
                    }
                    else
                    {
                        switch (activePortal)
                        {
                            case TileTypes.Earthportal:
                                levelNumber = 6;
                                break;
                            case TileTypes.Waterportal:
                                levelNumber = 11;
                                break;
                            case TileTypes.Airportal:
                                levelNumber = 16;
                                break;
                            case TileTypes.Fireportal:
                                levelNumber = 21;
                                break;
                        }
                    }
                    //activePortal = null;
                    activatedElementalPortal = false;
                }
                else
                {
                    ++levelNumber;
                }
                activatedPortal = false;               
            }

            TPC.Update(gameTime);

            foreach (GameEntity NPC in dead)
            {
                Entities.Remove(NPC);
            }
            dead.Clear();
            foreach (GameEntity NPC in Born)
            {
                Entities.Add(NPC);
            }
            Born.Clear();

            foreach (GameEntity entity in Entities)
            {
                entity.Update(gameTime);
            }

            healthbar.SetHealthbar(TPC.Tokens, TPC.AttackType);

            if (changeLevel)
            {
                soundEffects["portal"].Play();
                if (levelNumber < 26)   // yeah, it's a magic number
                {
                    GenerateLevel();
                    changeLevel = false;
                }
                else
                {
                    parentGame.GameOverScreen.EndState = Result.Win;
                    parentGame.ActivateScreen(parentGame.GameOverScreen);
                }
            }

            if (TPC.Tokens.Count == 0 && !Deathless)
            {
                parentGame.GameOverScreen.EndState = Result.Fail;
                parentGame.ActivateScreen(parentGame.GameOverScreen);
            }

            base.Update(gameTime);
        }

        /// <summary>
        /// Draws the level and entities on it.
        /// </summary>
        /// <param name="gameTime">Provides a snapshot of timing values.</param>
        public override void Draw(GameTime gameTime)
        {
            BoundingFrustum bf = new BoundingFrustum(Camera.View * Camera.Projection);
            foreach (TriDTile tile in Tiles)
            {
                if (tile != null && bf.Intersects(tile.BoundingBox))  //no pointless tiledrawing
                {
                    DrawModel(tile.Model, tile.World);                    
                }
            }

            DrawModel(TPC.Model, TPC.World);
            if (RenderBoundingSpheres)
            {
                BoundingSphereRenderer.Render(TPC.BoundingSphere, parentGame.GraphicsDevice, Camera.View, Camera.Projection, Color.Red,
                    Color.Green, Color.Blue);
                BoundingSphereRenderer.Render(TPC.GetAttackVolume(), parentGame.GraphicsDevice, Camera.View, Camera.Projection, Color.Red,
                    Color.Green, Color.Blue);
            }

            foreach (GameEntity NPC in Entities)
            {
                DrawModel(NPC.Model, NPC.World);
                if (RenderBoundingSpheres)
                {
                    BoundingSphereRenderer.Render(NPC.BoundingSphere, parentGame.GraphicsDevice, Camera.View, Camera.Projection, Color.Red,
                        Color.Green, Color.Blue);
                    BoundingSphereRenderer.Render(NPC.GetAttackVolume(), parentGame.GraphicsDevice, Camera.View, Camera.Projection, Color.Red,
                    Color.Green, Color.Blue);
                }
            }

            // for debugging
            /*if (Entities.Count > 0)
                Entities[0].Draw(gameTime);*/
            //TPC.Draw(gameTime);

            sb.Begin(SpriteBlendMode.AlphaBlend, SpriteSortMode.BackToFront,
                SaveStateMode.SaveState);
            sb.DrawString(font, "Lvl " + levelNumber,
                new Vector2((float)Game.Window.ClientBounds.Width - 70, 20), Color.White);
            sb.End();
            
            healthbar.Draw(gameTime);
            
            base.Draw(gameTime);
        }

        /// <summary>
        /// Method for drawing 3D models on screen.
        /// </summary>
        /// <param name="model">model to be drawn</param>
        /// <param name="world">world coordinates of the model</param>
        private void DrawModel(Model model, Matrix world)
        {
            // For drawing transparent models
            ContentManager contentManager = (ContentManager)parentGame.Services.GetService(typeof(ContentManager));
            contentManager.GraphicsDevice.RenderState.AlphaBlendEnable = true;
            contentManager.GraphicsDevice.RenderState.SourceBlend = Blend.SourceAlpha;
            contentManager.GraphicsDevice.RenderState.DestinationBlend = Blend.InverseSourceAlpha;

            Matrix[] transforms = new Matrix[model.Bones.Count];
            model.CopyAbsoluteBoneTransformsTo(transforms);

            foreach (ModelMesh mesh in model.Meshes)
            {
                foreach (BasicEffect effect in mesh.Effects)
                {
                    effect.EnableDefaultLighting();
                    effect.PreferPerPixelLighting = true;

                    effect.FogEnabled = true;                    
                    effect.FogColor = Color.Black.ToVector3();
                    effect.FogStart = 2000f;
                    effect.FogEnd = 3500f;

                    effect.World = transforms[mesh.ParentBone.Index] * world;

                    // Use the matrices provided by the camera
                    effect.View = Camera.View;
                    effect.Projection = Camera.Projection;
                }
                mesh.Draw();
            }

            contentManager.GraphicsDevice.RenderState.AlphaBlendEnable = false;
        }
        
        /// <summary>
        /// This method handles user input relating to control of the player character
        /// </summary>
        private void HandleInput()
        {
            KeyboardState keyboardState = Keyboard.GetState();
            MouseState mouseState = Mouse.GetState();
            Vector3 pos;
            Vector3 newposX;
            Vector3 newposZ;
            float rot;

            int mouseDiff = mouseState.X - Game.Window.ClientBounds.Width / 2;

            if (!Camera.Free)
            {
                if (mouseDiff > 15)
                    TPC.Rotation -= MathHelper.TwoPi / 45;
                else if (mouseDiff > 1)
                    TPC.Rotation -= MathHelper.TwoPi / 180;
                else if (mouseDiff < -1 && mouseDiff >= -15)
                    TPC.Rotation += MathHelper.TwoPi / 180;
                else if (mouseDiff < -15)
                    TPC.Rotation += MathHelper.TwoPi / 45;
            }

            Mouse.SetPosition(Game.Window.ClientBounds.Width / 2,
                                Game.Window.ClientBounds.Height / 2);

            if (oldMouseState.LeftButton == ButtonState.Pressed && mouseState.LeftButton == ButtonState.Released)
            {

                playerAttack = true;

                /*if (elapsedTime > attackDelay)
                {
                    if (TPC.CurrentAnim != "attack")
                    {
                        TPC.PlayOnce("attack");
                    }
                    foreach (GameEntity NPC in Entities)
                    {
                        if (TPC.GetAttackVolume().Intersects(NPC.BoundingSphere))
                        {
                            soundEffects["swordhit"].Play();
                            if (NPC.HandleHit(TPC.AttackType))  // if monster dead
                            {
                                dead.Add(NPC);
                                if (NPC.GetType() == typeof(Monster))
                                {
                                    Monster m = (Monster)NPC;
                                    ElementToken nt = new ElementToken(parentGame, m.GetElement());
                                    nt.Position = new Vector3(m.Position.X, 100, m.Position.Z);
                                    Born.Add(nt);   // monsters leave element tokens
                                }
                            }
                        }
                    }
                    elapsedTime = TimeSpan.FromMilliseconds(0.0);   // reset attack delay
                }*/
            }

            if (!console.Enabled)
            {
                if (keyboardState.IsKeyDown(Keys.W))
                {
                    if (TPC.CurrentFrame % 4 == 0 &&    // no good any way
                        !stepSoundPlayed)
                    {
                        soundEffects["playerWalk"].Play();
                        stepSoundPlayed = true;
                    }
                    else if (TPC.CurrentFrame % 2 != 0) stepSoundPlayed = false;

                    if (TPC.CurrentAnim != "walk")
                    {
                        TPC.CurrentAnim = "walk";
                    }
                    if (TPC.Stopped) TPC.Play();

                    rot = TPC.Rotation;                    
                    newposX = TPC.Position; // clean this
                    newposZ = TPC.Position;

                    newposX.X += (float)(20f * Math.Sin(rot));
                    newposZ.Z += (float)(20f * Math.Cos(rot));

                    AttemptMove(newposX.X, newposZ.Z);
                }

                if (keyboardState.IsKeyDown(Keys.S))
                {
                    if (TPC.CurrentAnim != "walk")
                    {
                        TPC.CurrentAnim = "walk";
                    }
                    if (TPC.Stopped) TPC.Play();

                    rot = TPC.Rotation;
                    pos = TPC.Position;

                    pos.X -= (float)(10f * Math.Sin(rot));
                    pos.Z -= (float)(10f * Math.Cos(rot));

                    AttemptMove(pos.X, pos.Z);
                }

                if (keyboardState.IsKeyDown(Keys.A))
                {
                    if (TPC.CurrentAnim != "walk")
                    {
                        TPC.CurrentAnim = "walk";
                    }
                    if (TPC.Stopped) TPC.Play();

                    rot = TPC.Rotation;
                    pos = TPC.Position;

                    pos.X -= (float)(10f * Math.Sin(rot-MathHelper.PiOver2));
                    pos.Z -= (float)(10f * Math.Cos(rot-MathHelper.PiOver2));

                    AttemptMove(pos.X, pos.Z);                                        
                }
                if (keyboardState.IsKeyDown(Keys.D))
                {
                    if (TPC.CurrentAnim != "walk")
                    {
                        TPC.CurrentAnim = "walk";
                    }
                    if (TPC.Stopped) TPC.Play();

                    rot = TPC.Rotation;
                    pos = TPC.Position;
                    Vector3 oldpos = pos;

                    pos.X -= (float)(10f * Math.Sin(rot+MathHelper.PiOver2));
                    pos.Z -= (float)(10f * Math.Cos(rot+MathHelper.PiOver2));

                    AttemptMove(pos.X, pos.Z);                    
                }

/*                if (keyboardState.IsKeyDown(Keys.E))    // take a peek at portal
                {
                    target = PortalPos;
                }
                else { target = TPC.Position; }*/
            }

            if (oldKeyboardState.IsKeyDown(Keys.Tab) && keyboardState.IsKeyUp(Keys.Tab))
            {
                console.Enabled = !console.Enabled;
                console.Visible = !console.Visible;
            }

            if (mouseState.LeftButton == ButtonState.Released
                && !(TPC.CurrentAnim == "attack" && !TPC.Stopped)
                && keyboardState.IsKeyUp(Keys.W)
                && keyboardState.IsKeyUp(Keys.S)
                && keyboardState.IsKeyUp(Keys.A)
                && keyboardState.IsKeyUp(Keys.D)
                && TPC.CurrentAnim != "idle")
            {
                TPC.CurrentAnim = "idle";
            }

            oldKeyboardState = keyboardState;
            oldMouseState = mouseState;
        }   // end HandleInput()

        /// <summary>
        /// Helper method for HandleInput().
        /// Attempts to perform requested movement.
        /// </summary>
        /// <param name="dx">component parallel to x-axis</param>
        /// <param name="dy">component parallel to z-axis</param>
        private void AttemptMove(float dx, float dy)
        {
            //float rot = TPC.Rotation;
            Vector3 oldpos = TPC.Position;

            Vector3 newposX = TPC.Position;
            Vector3 newposZ = TPC.Position;

            newposX.X = dx;
            newposZ.Z = dy;

            TPC.Position = new Vector3(newposX.X, newposX.Y, newposZ.Z);

            bool entityCollision = false;
            bool wallCollisionX = false;
            bool wallCollisionZ = false;

            foreach (GameEntity NPC in Entities)
            {
                if (CharacterCollision(TPC, NPC))
                {
                    entityCollision = true;
                    break;
                }
            }

            if (entityCollision)
            {
                TPC.Position = oldpos;
            }
            else
            {
                TPC.Position = newposX;
                wallCollisionX = WallCollision(TPC);

                TPC.Position = newposZ;
                wallCollisionZ = WallCollision(TPC);

                if (!wallCollisionX && !wallCollisionZ)
                {
                    TPC.Position = new Vector3(newposX.X, newposX.Y, newposZ.Z);
                }
                else if (!wallCollisionX)
                {
                    TPC.Position = newposX;
                }
                else if (!wallCollisionZ)
                {
                    TPC.Position = newposZ;
                }
                else
                {
                    TPC.Position = oldpos;
                }
            }
        }

        public bool WallCollision(GameEntity chara)
        {
            //TODO: Fix WallCollision()
            //WallCollision() shouldn't include any game logic code that has side-effects

            BoundingSphere cbs = chara.BoundingSphere;

            foreach (TriDTile tile in Tiles)
            {
                if (tile != null)
                {
                    if (tile.Type == TileTypes.Wall)
                    {
                        if (cbs.Intersects(tile.BoundingBox))
                            return true;
                    }
                    if (chara == TPC)
                    {
                        switch (tile.Type)
                        {
                            case TileTypes.Portal:
                                if (cbs.Intersects(tile.BoundingBox))
                                {
                                    /*if (!changeLevel)
                                    {
                                        ++levelNumber;
                                        changeLevel = true;
                                    }*/
                                    activatedPortal = true;
                                }
                                break;

                            case TileTypes.Earthportal:
                            case TileTypes.Airportal:
                            case TileTypes.Waterportal:
                            case TileTypes.Fireportal:
                                if (cbs.Intersects(tile.BoundingBox))
                                {
                                    activatedPortal = true;
                                    activatedElementalPortal = true;
                                    activePortal = tile.Type;

                                    /*if (levelNumber > 0)
                                    {                                        
                                        if (!changeLevel)
                                        {                                           
                                            ++levelNumber;
                                            changeLevel = true;
                                            if (levelNumber > TPC.Progress) //We've reached a new checkpoint
                                            {
                                                xmlEngine.CreateFile(".\\XML\\progress.xml");
                                                xmlEngine.StartElement("progress");
                                                xmlEngine.StartElement("level");
                                                xmlEngine.AddAttribute("value", levelNumber.ToString());
                                                xmlEngine.EndDocument();
                                            }
                                        }
                                    }*/
                                    /*else
                                    {
                                        //We are in the starter room
                                        changeLevel = true;
                                        switch (tile.Type)
                                        {
                                            case TileTypes.Earthportal:
                                                levelNumber = 6;
                                                break;
                                            case TileTypes.Waterportal:
                                                levelNumber = 11;
                                                break;
                                            case TileTypes.Airportal:
                                                levelNumber = 16;
                                                break;
                                            case TileTypes.Fireportal:
                                                levelNumber = 21;
                                                break;
                                        }                                                
                                    }*/
                                }
                                break;
                        }
                    }
                }
            }
            return false;
        }

        public bool CharacterCollision(GameEntity first, GameEntity second)
        {
            if (first.BoundingSphere.Intersects(second.BoundingSphere))
            {
                if ( second.GetType() == typeof(ElementToken) )
                {
                    //if (first == TPC)   // pick up token
                    if (first.GetType() == typeof(PlayerCharacter))
                    {
                        tokenPickUp = (ElementToken)second;

                        /*ElementToken e = (ElementToken)second;
                        first.AddToken(e.GetElement());
                        dead.Add(second);
                        soundEffects["powerup"].Play();*/
                    }
                    return false;   // monsters walk through pickups
                }
                return true;
            }
            else
                return false;
        }

        public void GenerateLevel()
        {
            Tiles = new TriDTile[50, 50];
            
            pyEngine.SetVariable("mapSize", 45);
            pyEngine.SetVariable("cellSize", 10);
            pyEngine.SetVariable("levelNumber", levelNumber);

            if (levelNumber > 0)
            {
                pyEngine.RunScript("Level generator");
            }
            else pyEngine.RunScript("Starting level generator");
        }

        protected override void LoadContent()
        {
            log.Write("Level.LoadContent()");
            ContentManager contentManager = (ContentManager)parentGame.Services.GetService(typeof(ContentManager));
            sb = contentManager.SpriteBatch;
            if (sb == null) log.Write("Level SpriteBatch load failed");

            soundEffects = new Dictionary<string, SoundEffect>();
            soundEffects["playerWalk"] = contentManager.SoundEffects["playerWalk"];
            soundEffects["powerup"] = contentManager.SoundEffects["powerup"];
            soundEffects["swordhit"] = contentManager.SoundEffects["swordhit"];
            soundEffects["portal"] = contentManager.SoundEffects["portal"];
            base.LoadContent();
        }
    }
}
